package org.sensornet2010.co2sh.core;

import java.io.IOException;
import java.util.Hashtable;

public class SoundDecoder {

	private RemoteServer server;
	public static final int FFT_RESOLUTION = 1024;
	
	public SoundDecoder(RemoteServer server) {
		this.server = server;
	}
	
	/**
	 * Decodes sound into CO2 value
	 * 
	 * @param sound		byte[]
	 * @return co2Value	integer
	 */
	public int decodeSound(byte[] sound) {
		SensornetUtil.debug("decodeSound()");
		int co2Value = -1;
		try{
			byte[] wavSound = this.convertAmr2Wav(sound);
			Hashtable wavObject = parseWav(wavSound);
			co2Value = decodeSingleFrequencyEncoding(wavObject);			
		}catch (Exception e) {
			SensornetUtil.debugException(e);
			// TODO decodeSound() implement error handling
		}
		return co2Value;
	}	


	/**
	 * Converts the audio signal from AMR to WAV format.
	 */
	protected byte[] convertAmr2Wav(byte[] amrSound) {
		// implementation using the RemoteServer class
		return this.server.convertAmr2Wav(amrSound);
	}

	
	/**
	 * Algorithm that decodes the information from the sound signal
	 * @param wavContent		byte[]
	 * @return findMaximum(X)	integer
	 */
	protected int decodeSingleFrequencyEncoding(Hashtable wav) {
		double[] wavContent = (double[]) wav.get("DATA");
		int samplingFrequency = Integer.parseInt(wav.get("SAMPLING_RATE").toString());
		double[] X = fft(wavContent, FFT_RESOLUTION);
		int max = findMaximum(X);
		double f = (max) * samplingFrequency /2 * 1 / (FFT_RESOLUTION/2 -1)  ;
		int frequency = (int)f;
		return frequency;
	}
	
	private int findMaximum(double[] X) {
		// calculates the position of the maximum value in the array 
			int i, pos = 0;
			double value = 0;
			for (i = 0; i < X.length; i++) {
				if (X[i] > value) {
					value = X[i];
					pos = i;
				}
			}
			return pos;
		}

	

	/**
	 * Returns an array with modules of frequency components (N = precision)
	 * 
	 * @param x
	 *            double[]
	 * @param N
	 *            integer
	 * @return X double[]
	 */
	public static double[] fft(double[] x, int N) {
		double[] X = new double[N];
		int j, f;
		double real = 0, imaginary = 0, realSum = 0, imaginarySum = 0;
		for (f = 0; f < N / 2; f++) {
			for (j = 0; j < N; j++) {
				real = x[j] * Math.cos((2 * Math.PI * j * f) / N);
				realSum = real + realSum;
				imaginary = x[j] * (-1) * Math.sin((2 * Math.PI * j * f) / N);
				imaginarySum = imaginary + imaginarySum;
			}
			X[f] = Math.sqrt(realSum * realSum + imaginarySum * imaginarySum);
			realSum = 0;
			imaginarySum = 0;
		}
		return X;
	}

	public static Hashtable parseWav(byte[] wavBinaryContent){
		Hashtable wavObject = new Hashtable();
		try {
			AbstractSoundStream sound = new SoundStreamNokia(wavBinaryContent);
			int bitsXsample = sound.getBitsPerSample();
			long samplingRate = sound.getSampleRate();
			int start = 0;
			int end = FFT_RESOLUTION;//;(int) sound.getDataSize();
			// TODO return all the sampled data available
			double[] data = sound.readSamples(start, end);
			wavObject.put("SAMPLING_RATE", samplingRate);
			wavObject.put("DATA", data);
		}catch (Exception e) {
			throw new RuntimeException(e);
		}
		return wavObject;
	}

	private static class SoundStreamNokia extends AbstractSoundStream {
		// Code snippet from NoiseTube
		// http://www.google.com/codesearch/p?hl=en#dZj8F8afubQ/trunk/mobileapp/java/src/noisetube/audio/java/AbstractSoundStream.java

		public SoundStreamNokia(byte[] s) throws Exception {
			super(s);
		}

		protected byte readByte(int l) throws Exception {
			return data[l];
		}

		protected int readInt(int i) throws Exception {
			return (int) byteArrayToLong(data, i);
		}

		/**
		 * @param l
		 *            index of the sample
		 * @return
		 * @throws IOException
		 */
		protected int readShort(int l) throws Exception {
			return (short) ((data[l + 1] & 0xff) << 8 | data[l] & 0xff);
		}

		// ===========================
		// CONVERT BYTES TO JAVA TYPES
		// ===========================

		// these two routines convert a byte array to a unsigned short
		public int byteArrayToInt(byte[] b, int start) {
			int low = b[start] & 0xff;
			int high = b[start + 1] & 0xff;
			return (short) (high << 8 | low);
		}

		// these two routines convert a byte array to an unsigned integer
		public long byteArrayToLong(byte[] b, int start) {

			int i = 0;
			int len = 4;
			int cnt = 0;
			byte[] tmp = new byte[len];
			for (i = start; i < (start + len); i++) {
				tmp[cnt] = b[i];
				cnt++;
			}
			long accum = 0;
			i = 0;
			for (int shiftBy = 0; shiftBy < 32; shiftBy += 8) {
				accum |= ((long) (tmp[i] & 0xff)) << shiftBy;
				i++;
			}
			return accum;
		}
	}

	private static abstract class AbstractSoundStream {

		private int format;
		private int channels;
		private long chunkSize;
		private long sampleRate;
		private long byteRate;
		private int blockAlign;
		private int bitsPerSample;
		private long dataSize;
		protected byte[] data;
		private boolean header_read = false;
		final int HEADER_SIZE = 44;

		/**
		 * Read the sound file from a array of byte
		 * 
		 * @param stream
		 * @throws IOException
		 */
		public AbstractSoundStream(byte[] stream) throws Exception {

			if (stream == null) {
				throw new IllegalArgumentException("stream null");
			}

			if (stream.length <= HEADER_SIZE) {
				throw new IllegalArgumentException("Stream not valid (only "
						+ stream.length + ")");
			}

			dataSize = stream.length - HEADER_SIZE;
			data = stream;
		}

		private void readHeader() {
			try {
				// skip the chunkID
				String chunkID = "" + (char) readByte(0) + (char) readByte(1)
						+ (char) readByte(2) + (char) readByte(3);

				String formatS = "" + (char) readByte(8) + (char) readByte(9)
						+ (char) readByte(10) + (char) readByte(11);

				// read the audio format. This should be 1 for PCM
				format = readShort(20);

				// read the # of channels (1 or 2)
				channels = readShort(22);

				// read the samplerate
				sampleRate = readInt(24);

				// read the bitspersample
				bitsPerSample = readShort(34);

				blockAlign = channels * bitsPerSample / 8;

				// read the byterate
				byteRate = blockAlign * sampleRate;

				// read the bitspersample
				chunkSize = readInt(40);

				// fix bug
				if (chunkSize < dataSize) {

					dataSize = chunkSize;
				}

				header_read = true;

			} catch (Exception ignore) {
				ignore.printStackTrace();
			}
		}

		public double[] readSamples2(int start, int end) throws Exception {
			double[] samples = new double[end - start + 1];
			for (int i = 0; i < samples.length; i++) {
				int start_byte = getByteIndex(start + i);
				// if (d > 32767 || d<0) allowed?
				int low = data[start_byte] & 0xff;
				int hi = data[start_byte + 1] << 8;
				samples[i] = (((double) (hi | low)) / (double) (Short.MAX_VALUE));
			}
			return samples;
		}

		public double[] readSamples(int start, int end) throws Exception {

			double[] samples = new double[end - start + 1];

			// for each sample
			for (int i = 0; i < samples.length; i++) {

				int start_byte = getByteIndex(start + i);

				int d = readShort(start_byte);

				samples[i] = (d) / 32768D;
			}
			return samples;
		}

		public byte[] getStream() {
			return data;
		}

		public int getFormat() {
			if (!header_read) {
				readHeader();
			}
			return format;
		}

		public long getChannels() {
			if (!header_read) {
				readHeader();
			}
			return channels;
		}

		public long getSampleRate() {
			if (!header_read) {
				readHeader();
			}
			return sampleRate;
		}

		public long getByteRate() {
			if (!header_read) {
				readHeader();
			}
			return byteRate;
		}

		public int getBlockAlign() {
			if (!header_read) {
				readHeader();
			}
			return blockAlign;
		}

		public int getBitsPerSample() {
			if (!header_read) {
				readHeader();
			}
			return bitsPerSample;
		}

		public long getDataSize() {
			if (!header_read) {
				readHeader();
			}
			return dataSize;
		}

		public String toString() {
			return "format: " + format + "\nsampleRate: " + sampleRate
					+ "\nchannels: " + channels + "\nbyteRate: " + byteRate
					+ "\nbitsPerSample: " + bitsPerSample + "\nblockAlign: "
					+ blockAlign + "\nchunkSize: " + chunkSize + "\ndataSize: "
					+ dataSize;
		}

		protected abstract byte readByte(int l) throws Exception;

		protected abstract int readShort(int l) throws Exception;

		protected abstract int readInt(int i) throws Exception;

		private int getByteIndex(int sample_idx) {
			return HEADER_SIZE + sample_idx * blockAlign;
		}

	}
	
}
